#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <windows.h>

// Configuration
#define windowRawW 320
#define windowRawH 200
#define windowScale 4
#define windowRealW (windowRawW * windowScale)
#define windowRealH (windowRawH * windowScale)

#define worldSX 40
#define worldSY 4
#define worldSZ 40

#define maxTextureNum 100
#define maxSurfaceNum 100
#define maxEntityNum 100

#define bgColor RGB32(0, 0, 0)

#define textureDPI 64

#define hFov 90
#define moveSpeed 8
#define viewSpeed 2

// Macros
#define M_PI 3.1415927f

#define RGB32(r, g, b) ((0x00 << 24) | ((r) << 16) | ((g) << 8) | (b))
#define ALPHA(col) (((col) & 0xff000000) >> 24)
//#define RED(col) (((col) & 0x00ff0000) >> 16)
//#define GREEN(col) (((col) & 0x0000ff00) >> 8)
//#define BLUE(col) ((col) & 0x000000ff)

// Types
typedef unsigned int Uint32;

typedef struct {
    float x, y, z;
} vec3;

typedef struct {
    int hitSurface, hitEntity;
    float dist;
    float x, y, z;
    float nx, ny, nz;
} HitInfo;

typedef struct {
    int width, height;
    Uint32 *data;
} Texture;

typedef struct {
    int texID;
    vec3 origin, horizontal, vertical, normal;
    float width, height;
} Surface;

typedef struct {
    int texID;
    vec3 origin;
    float width, height;
} Entity;

// Functions
void handleInput(int key, int down);
void shoot();
void update(float dt);
void handleCollision(vec3 *pos);
void drawFrame();
void scaleFrame();

float distance(vec3 *a, vec3 *b);
Uint32 raytrace(vec3 *pos, vec3 *dir, HitInfo *info);
void rayDir(int x, int y, vec3 *d);
int intersectSurface(Surface *surface, vec3 *rayOri, vec3 *rayDir, vec3 *inter);
int intersectEntity(Entity *entity, vec3 *rayOri, vec3 *rayDir, vec3 *inter);
int textureSurface(Surface *surface, vec3 *inter, Uint32 *color);
int textureEntity(Entity *entity, vec3 *rayDir, vec3 *inter, Uint32 *color);

// Resources
#define T_hudW windowRawW
#define T_hudH windowRawH
extern Uint32 T_hud[windowRawH * windowRawW];
#define T_tileW 64
#define T_tileH 64
#define T_floorID 0
extern Uint32 T_floor[T_tileH * T_tileW];
#define T_wallID 1
extern Uint32 T_wall[T_tileH * T_tileW];
#define T_ceilingID 2
extern Uint32 T_ceiling[T_tileH * T_tileW];
#define T_entityW 53
#define T_entityH 72
#define T_enemyID 3
extern Uint32 T_enemy[T_entityH * T_entityW];
#define T_enemy_deadID 4
extern Uint32 T_enemy_dead[T_entityH * T_entityW];

// Constants
float vFov, rFov;

// Globals
Uint32 pixels[windowRawW * windowRawH];
Uint32 pixelsScaled[windowRealW * windowRealH];

// World
int textureNum = 0;
Texture textures[maxTextureNum] = {
    {T_tileW, T_tileH, T_floor},
    {T_tileW, T_tileH, T_wall},
    {T_tileW, T_tileH, T_ceiling},
    {T_entityW, T_entityH, T_enemy},
    {T_entityW, T_entityH, T_enemy_dead}
};
int surfaceNum = 9;
Surface surfaces[maxSurfaceNum] = {
    {T_floorID, {0, 0, 0}, {1, 0, 0}, {0, 0, 1}, {0, 1, 0}, worldSX, worldSZ},
    {T_wallID, {0, 0, 0}, {1, 0, 0}, {0, 1, 0}, {0, 0, 1}, worldSX, worldSY},
    {T_wallID, {0, 0, 0}, {0, 0, 1}, {0, 1, 0}, {1, 0, 0}, worldSZ, worldSY},
    {T_wallID, {worldSX, 0, 0}, {0, 0, 1}, {0, 1, 0}, {-1, 0, 0}, worldSZ, worldSY},
    {T_wallID, {0, 0, worldSZ}, {1, 0, 0}, {0, 1, 0}, {0, 0, -1}, worldSX, worldSY},
    {T_ceilingID, {0, worldSY, 0}, {1, 0, 0}, {0, 0, 1}, {0, -1, 0}, worldSX, worldSZ},
    {T_wallID, {16, 0, 0}, {0, 0, 1}, {0, 1, 0}, {1, 0, 0}, 5, 3},
    {T_wallID, {14, 0, 5}, {0, 0, -1}, {0, 1, 0}, {-1, 0, 0}, 5, 3},
    {T_wallID, {14, 0, 5}, {1, 0, 0}, {0, 1, 0}, {0, 0, 1}, 2, 3}
};
int entityNum = 8;
Entity entities[maxEntityNum] = {
    {T_enemyID, {10, 0, 10}, T_entityW * 3 / (float)textureDPI, T_entityH * 3 / (float)textureDPI},
    {T_enemyID, {30, 0, 10}, T_entityW * 3 / (float)textureDPI, T_entityH * 3 / (float)textureDPI},
    {T_enemyID, {10, 0, 30}, T_entityW * 3 / (float)textureDPI, T_entityH * 3 / (float)textureDPI},
    {T_enemyID, {30, 0, 30}, T_entityW * 3 / (float)textureDPI, T_entityH * 3 / (float)textureDPI},
    {T_enemyID, {20, 0, 10}, T_entityW * 3 / (float)textureDPI, T_entityH * 3 / (float)textureDPI},
    {T_enemyID, {10, 0, 20}, T_entityW * 3 / (float)textureDPI, T_entityH * 3 / (float)textureDPI},
    {T_enemyID, {30, 0, 20}, T_entityW * 3 / (float)textureDPI, T_entityH * 3 / (float)textureDPI},
    {T_enemyID, {20, 0, 30}, T_entityW * 3 / (float)textureDPI, T_entityH * 3 / (float)textureDPI}
};

// Player
vec3 playerPos = {20.0f, 2.0f, 20.0f};
vec3 velocity = {0, 0, 0};

// The sine and cosine are the same for all pixels
float pitch = 0.0f;
float pitchC = 1.0f;
float pitchS = 0.0f;

float yaw = 0.0f;
float yawC = 1.0f;
float yawS = 0.0f;

// Input
float dPitch = 0.0f;
float dYaw = 0.0f;

int keyLEFT = 0;
int keyRIGHT = 0;
int keyA = 0;
int keyW = 0;
int keyS = 0;
int keyD = 0;
int keyESC = 0;


// Init all constants which cannot be statically defined
void initConst() {
    // Calculate vertical fov and fov constant from specified horizontal fov
    vFov = 2.0f * atanf(tanf(hFov / 720.0f * M_PI) * 320.0f / 200.0f);
    rFov = tanf(0.5f * vFov);
}


#pragma region  ---------- Windows API Part ----------

// Call back function for window object
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
    switch (msg) {
    case WM_KEYDOWN:
        handleInput((int)wParam, 1);
        break;
    case WM_KEYUP:
        handleInput((int)wParam, 0);
        break;
    case WM_CLOSE:
        DestroyWindow(hwnd);
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

// The real main function
int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow) {

    initConst();

    WNDCLASSEX wc;
    HWND hwnd;
    MSG msg;
    HDC hdc, memdc;
    HBITMAP hbitmap, hbmback;

    const char g_szClassName[] = "AsmDuum";

    wc.cbSize = sizeof(WNDCLASSEX);
    wc.style = 0;
    wc.lpfnWndProc = WndProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hInstance;
    wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wc.lpszMenuName = NULL;
    wc.lpszClassName = g_szClassName;
    wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);

    if (!RegisterClassEx(&wc)) {
        MessageBox(NULL, "Window Registration Failed!", "Error!",
            MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

    hwnd = CreateWindowEx(
        WS_EX_CLIENTEDGE,
        g_szClassName,
        "The title of my window",
        WS_OVERLAPPEDWINDOW ^ WS_THICKFRAME ^ WS_MAXIMIZEBOX,
        CW_USEDEFAULT, CW_USEDEFAULT, windowRealW, windowRealH,
        NULL, NULL, hInstance, NULL);

    if (hwnd == NULL) {
        MessageBox(NULL, "Window Creation Failed!", "Error!",
            MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

    RECT WindowRect;
    RECT ClientRect;
    GetWindowRect(hwnd, &WindowRect);
    GetClientRect(hwnd, &ClientRect);
    WindowRect.right += (windowRealW - ClientRect.right);
    WindowRect.bottom += (windowRealH - ClientRect.bottom);
    MoveWindow(hwnd, WindowRect.left, WindowRect.top, WindowRect.right - WindowRect.left, WindowRect.bottom - WindowRect.top, 1);

    hdc = GetDC(hwnd);
    memdc = CreateCompatibleDC(hdc);
    hbitmap = CreateCompatibleBitmap(hdc, windowRealW, windowRealH);
    hbmback = (HBITMAP)SelectObject(memdc, hbitmap);

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    int frames = 0;
    int lsec = 0;
    char buf[128];
    float lastUpdate = 0.0f;

    while (1) {
        if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
            if (msg.message == WM_QUIT) {
                break;
            }
            else {
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
        }

        if (keyESC) break;

        // Update
        float now = GetTickCount() / 1000.0f;
        update(now - lastUpdate);
        lastUpdate = now;

        // Draw frame
        drawFrame();
        scaleFrame();
        SetBitmapBits(hbitmap, windowRealW * windowRealH * 4, pixelsScaled);
        BitBlt(hdc, 0, 0, windowRealW, windowRealH, memdc, 0, 0, SRCCOPY);

        frames++;
        if (lsec != GetTickCount() / 1000) {
            sprintf_s(buf, sizeof(buf), "AsmDuum (FPS: %d) %f: %f, %f, %f", frames, yaw, playerPos.x, playerPos.y, playerPos.z);
            SetWindowText(hwnd, buf);
            frames = 0;
            lsec = GetTickCount() / 1000;
        }
    }

    return (int)msg.wParam;
}

#pragma endregion


#pragma region ---------- Engine Part ----------

void handleInput(int key, int down) {
    switch (key) {
    case VK_LEFT:
        keyLEFT = down;
        break;
    case VK_RIGHT:
        keyRIGHT = down;
        break;
    case 'A':
        keyA = down;
        break;
    case 'W':
        keyW = down;
        break;
    case 'S':
        keyS = down;
        break;
    case 'D':
        keyD = down;
        break;
    case VK_SPACE:
        if (down) {
            shoot();
        }
        break;
    case VK_ESCAPE:
        keyESC = down;
        break;
    default: break;
    }
}

void shoot() {
    HitInfo info;
    vec3 dir;
    rayDir(windowRawW / 2, windowRawH / 2, &dir);
    raytrace(&playerPos, &dir, &info);

    if (info.hitEntity >= 0) {
        entities[info.hitEntity].texID = T_enemy_deadID;
    }
}

void update(float dt) {
    // Update view
    if (keyLEFT) {
        yaw += viewSpeed * dt;
    }
    if (keyRIGHT) {
        yaw -= viewSpeed * dt;
    }
    yawS = sinf(yaw);
    yawC = cosf(yaw);

    // Set X/Z velocity depending on input
    velocity.x = velocity.z = 0.0f;
    if (keyA) {
        velocity.x += moveSpeed * cosf(M_PI - yaw);
        velocity.z += moveSpeed * sinf(M_PI - yaw);
    }
    if (keyW) {
        velocity.x += moveSpeed * cosf(-M_PI / 2 - yaw);
        velocity.z += moveSpeed * sinf(-M_PI / 2 - yaw);
    }
    if (keyS) {
        velocity.x += moveSpeed * cosf(M_PI / 2 - yaw);
        velocity.z += moveSpeed * sinf(M_PI / 2 - yaw);
    }
    if (keyD) {
        velocity.x += moveSpeed * cosf(-yaw);
        velocity.z += moveSpeed * sinf(-yaw);
    }

    // Handle block collision (head, lower body and feet)
    vec3 headPos = playerPos;
    vec3 lowerPos = playerPos; lowerPos.y -= 1.0f;
    vec3 footPos = playerPos; footPos.y -= 2.0f;
    handleCollision(&headPos);
    handleCollision(&lowerPos);
    handleCollision(&footPos);

    // Apply motion
    playerPos.x += velocity.x * dt;
    playerPos.z += velocity.z * dt;
    if (playerPos.x < 0) playerPos.x = 0.01f;
    if (playerPos.x >= worldSX) playerPos.x = worldSX - 0.01f;
    if (playerPos.z < 0) playerPos.z = 0.01f;
    if (playerPos.z >= worldSZ) playerPos.z = worldSZ - 0.01f;
}

void handleCollision(vec3 *pos) {
    HitInfo info;
    raytrace(pos, &velocity, &info);

    // For convenience, you don't collide with enemies
    if (info.hitSurface >= 0 && info.dist < 1.0f) {
        if (info.nx != 0) velocity.x = 0.0f;
        if (info.ny != 0) velocity.y = 0.0f;
        if (info.nz != 0) velocity.z = 0.0f;
    }
}

void drawFrame() {
    vec3 ray;
    for (int x = 0; x < windowRawW; ++x) {
        for (int y = 0; y < windowRawH; ++y) {
            Uint32 color = T_hud[y * windowRawW + x];
            // If hud not present
            if (ALPHA(color) != 0) {
                rayDir(x, y, &ray);
                color = raytrace(&playerPos, &ray, NULL);
            }
            pixels[y * windowRawW + x] = color;
        }
    }
}

void scaleFrame() {
    for (int x = 0; x < windowRealW; ++x) {
        for (int y = 0; y < windowRealH; ++y) {
            int xx = x / windowScale, yy = y / windowScale;
            pixelsScaled[y * windowRealW + x] = pixels[yy * windowRawW + xx];
        }
    }
}

#pragma endregion


#pragma region ---------- Rendering Part ----------

float distance(vec3 *a, vec3 *b) {
    float x = a->x - b->x, y = a->y - b->y, z = a->z - b->z;
    return sqrtf(x * x + y * y + z * z);
}

// Returns final color
Uint32 raytrace(vec3 *pos, vec3 *dir, HitInfo *info) {
    // Finish early if there's no direction
    if (dir->x == 0.0f && dir->y == 0.0f && dir->z == 0.0f) {
        goto nohit;
    }

    float x = pos->x;
    float y = pos->y;
    float z = pos->z;

    // Assumption is made that the camera is never outside the world
    if (x < 0 || y < 0 || z < 0 || x >= worldSX || y >= worldSY || z >= worldSZ) {
        goto nohit;
    }

    float dist = 0.0f;
    Uint32 finalColor = bgColor;

    int hitSurface = -1;
    for (int i = 0; i < surfaceNum; ++i) {
        vec3 inter;
        Uint32 color;
        if (intersectSurface(&surfaces[i], pos, dir, &inter) && textureSurface(&surfaces[i], &inter, &color)) {
            float distNow = distance(pos, &inter);
            if (hitSurface < 0 || distNow < dist) {
                dist = distNow;
                hitSurface = i;
                finalColor = color;
                // Hit info consider surfaces only
                if (info != NULL) {
                    info->x = inter.x;
                    info->y = inter.y;
                    info->z = inter.z;
                    info->nx = surfaces[i].normal.x;
                    info->ny = surfaces[i].normal.y;
                    info->nz = surfaces[i].normal.z;
                }
            }
        }
    }

    int hitEntity = -1;
    for (int i = 0; i < entityNum; ++i) {
        vec3 inter;
        Uint32 color;
        if (intersectEntity(&entities[i], pos, dir, &inter) && textureEntity(&entities[i], dir, &inter, &color)) {
            float distNow = distance(pos, &inter);
            if (distNow < dist) {
                dist = distNow;
                hitEntity = i;
                hitSurface = -1;
                // Deal with transparent pixels in entity's texture
                if (ALPHA(color) == 0) {
                    finalColor = color;
                }
            }
        }
    }

    if (info != NULL) {
        info->hitSurface = hitSurface;
        info->hitEntity = hitEntity;
        info->dist = dist;
    }
    return finalColor;

nohit:
    if (info != NULL) {
        info->hitSurface = -1;
        info->hitEntity = -1;
    }
    return bgColor;
}

// Calculate fov ray dir
void rayDir(int sx, int sy, vec3 *d) {
    // This is simply a precomputed version of the actual linear transformation,
    // which is the inverse of the common view and 
    // projection transformation used in rasterization.
    float clipX = sx / 160.0f - 1.0f;
    float clipY = 1.0f - sy / 100.0f;

    float x = 1.6f * rFov * yawC * clipX + rFov * yawS * pitchS * clipY - pitchC * yawS;
    float y = rFov * pitchC * clipY + pitchS;
    float z = -1.6f * rFov * yawS * clipX + rFov * yawC * pitchS * clipY - pitchC * yawC;

    // Normalize
    float length = sqrtf(x * x + y * y + z * z);
    x /= length;
    y /= length;
    z /= length;

    d->x = x;
    d->y = y;
    d->z = z;
}

// Calculate intersection point with surfaces
// 0 indicates no valid intersection point found
int intersectSurface(Surface *surface, vec3 *rayOri, vec3 *rayDir, vec3 *inter) {
    float vp1 = surface->normal.x, vp2 = surface->normal.y, vp3 = surface->normal.z;
    float n1 = surface->origin.x, n2 = surface->origin.y, n3 = surface->origin.z;
    float v1 = rayDir->x, v2 = rayDir->y, v3 = rayDir->z;
    float m1 = rayOri->x, m2 = rayOri->y, m3 = rayOri->z;
    if (vp1 == 0 && vp2 == 0 && vp3 == 0) return 0;
    if (v1 == 0 && v2 == 0 && v3 == 0) return 0;

    float vpt = v1 * vp1 + v2 * vp2 + v3 * vp3;
    // You can't see surfaces that do not face toward you
    if (vpt >= 0) return 0;

    float t = ((n1 - m1) * vp1 + (n2 - m2) * vp2 + (n3 - m3) * vp3) / vpt;

    inter->x = m1 + v1 * t;
    inter->y = m2 + v2 * t;
    inter->z = m3 + v3 * t;

    // You can't see things behind you
    if ((inter->x - m1) / v1 < 0 || (inter->z - m3) / v3 < 0) return 0;
    return 1;
}

// Calculate intersection point with entities
// 0 indicates no valid intersection point found
int intersectEntity(Entity *entity, vec3 *rayOri, vec3 *rayDir, vec3 *inter) {
    // Entities are always facing you
    float vp1 = -rayDir->x, vp2 = 0, vp3 = -rayDir->z;
    float n1 = entity->origin.x, n2 = entity->origin.y, n3 = entity->origin.z;
    float v1 = rayDir->x, v2 = rayDir->y, v3 = rayDir->z;
    float m1 = rayOri->x, m2 = rayOri->y, m3 = rayOri->z;
    if (vp1 == 0 && vp2 == 0 && vp3 == 0) return 0;
    if (v1 == 0 && v2 == 0 && v3 == 0) return 0;

    float vpt = v1 * vp1 + v2 * vp2 + v3 * vp3;
    float t = ((n1 - m1) * vp1 + (n2 - m2) * vp2 + (n3 - m3) * vp3) / vpt;

    inter->x = m1 + v1 * t;
    inter->y = m2 + v2 * t;
    inter->z = m3 + v3 * t;

    // You can't see things behind you
    if ((inter->x - m1) / v1 < 0 || (inter->z - m3) / v3 < 0) return 0;
    return 1;
}

// Get texture from surfaces
int textureSurface(Surface *surface, vec3 *inter, Uint32 *color) {
    float x = inter->x - surface->origin.x;
    float y = inter->y - surface->origin.y;
    float z = inter->z - surface->origin.z;
    float hx = surface->horizontal.x;
    float hy = surface->horizontal.y;
    float hz = surface->horizontal.z;
    float vx = surface->vertical.x;
    float vy = surface->vertical.y;
    float vz = surface->vertical.z;

    float tx = x * hx + y * hy + z * hz;
    float ty = x * vx + y * vy + z * vz;
    if (tx < 0 || tx >= surface->width || ty < 0 || ty >= surface->height) {
        return 0;
    }

    int texW = textures[surface->texID].width;
    int texH = textures[surface->texID].height;
    int texX = (int)(tx * textureDPI) % texW;
    int texY = (int)(ty * textureDPI) % texH;
    // Flip texture because of different coord between texture and world
    texY = texH - texY - 1;
    // 
    //if (texX < 0 || texX >= texW || texY < 0 || texY >= texH) {
    //    return 0;
    //}

    *color = textures[surface->texID].data[texY * texW + texX];
    return 1;
}

// Get texture from entities
int textureEntity(Entity *entity, vec3 *rayDir, vec3 *inter, Uint32 *color) {
    float x = inter->x - entity->origin.x;
    float y = inter->y - entity->origin.y;
    float z = inter->z - entity->origin.z;
    float hx = -rayDir->z;
    float hy = 0.0f;
    float hz = rayDir->x;
    float vx = 0.0f;
    float vy = 1.0f;
    float vz = 0.0f;

    float tx = x * hx + y * hy + z * hz;
    float ty = x * vx + y * vy + z * vz;
    // The origin of entity shall be at the mid point horizontally
    tx += entity->width / 2;
    //if (tx < 0 || tx >= entity->width || ty < 0 || ty >= entity->height) {
    //    return 0;
    //}

    int texW = textures[entity->texID].width;
    int texH = textures[entity->texID].height;
    int texX = (int)(tx / entity->width * texW);
    int texY = (int)(ty / entity->height * texH);
    // Flip texture because of different coord between texture and world
    texY = texH - texY - 1;
    if (texX < 0 || texX >= texW || texY < 0 || texY >= texH) {
        return 0;
    }

    *color = textures[entity->texID].data[texY * texW + texX];
    return 1;
}

#pragma endregion